Skip to content

feat: BTC-only firmware build support#388

Open
BitHighlander wants to merge 26 commits intodevelopfrom
feat/btc-only-build
Open

feat: BTC-only firmware build support#388
BitHighlander wants to merge 26 commits intodevelopfrom
feat/btc-only-build

Conversation

@BitHighlander
Copy link
Collaborator

Motivation

A BTC-only firmware variant reduces attack surface for users who only hold Bitcoin. Every alt-chain message handler, signing implementation, and proto parser is code that doesn't need to exist on a Bitcoin-only device. Removing it:

  1. Reduces flash footprint — frees space for future Bitcoin features (Taproot, Miniscript)
  2. Reduces attack surface — fewer message handlers means fewer entry points for potential vulnerabilities
  3. Simplifies audit scope — a BTC-only build can be audited against a much smaller codebase

This follows the pattern established by Trezor (which ships separate BTC-only firmware) and Coldcard (BTC-only by design).

Changes

Build system (CMakeLists.txt at multiple levels):

  • New cmake variable COIN_SUPPORT — set to BTC for Bitcoin-only, unset for full multichain
  • lib/transport/CMakeLists.txt: conditional proto compilation — BTC-only builds only compile types.proto + messages.proto, skipping all alt-chain protos
  • lib/firmware/CMakeLists.txt: alt-chain source files added via list(APPEND) inside if(NOT COIN_SUPPORT BTC) guard
  • tools/firmware/CMakeLists.txt, unittests/: matching conditional compilation

Source guards:

  • #ifndef BITCOIN_ONLY / #endif around alt-chain includes in fsm.c, interface.h
  • Message map entries guarded in messagemap.def
  • fsm_msg_common.h: token enumeration guarded (BTC-only firmware has no ERC-20 tokens)
  • fsm.h: alt-chain function declarations guarded

BTC variant:

  • lib/variant/keepkey/logobtc.c — Bitcoin-branded boot logo
  • Docker compose and build scripts for BTC-only emulator and device images

No changes to the full multichain build path. When COIN_SUPPORT is not set to BTC, all existing behavior is preserved — the guards compile out completely.

Risk assessment

Moderate complexity but low risk to existing builds. The #ifndef BITCOIN_ONLY pattern is purely additive to the full build (the define is never set in the normal build). The conditional CMakeLists use list(APPEND) which doesn't modify the base lists.

The main risk is missing a guard somewhere, causing a BTC-only build to reference an alt-chain symbol. This manifests as a link error (fail-fast, not silent). The CI runs both full and BTC-only builds to catch this.

Testing

  • Full multichain build: ARM + emulator pass (no regression)
  • BTC-only build: compiles and links (proto check passes with only types.pb.h + messages.pb.h)
  • Python integration tests pass (test BTC functionality, alt-chain tests skip)
  • Unit tests pass with conditional test exclusion

- Point device-protocol and python-keepkey submodules to upstream master
  (includes BIP-85, Solana, Tron, TON wire IDs and proto definitions)
- Add nanopb .options files for Solana, Tron, TON (field size constraints)
- Add Bip85Mnemonic.mnemonic max_size:241 to messages.options
- Update lib/transport/CMakeLists.txt with new proto sources, options,
  headers, and protoc compilation commands
- Fix CI: use pre-installed clang-format instead of apt-get install
  (eliminates 3-minute timeout on GitHub runners)
- Update Zcash transparent branch ID from Sapling to NU6
BitHighlander and others added 9 commits March 16, 2026 21:16
…double-hash (F3)

Replace early-return ECDSA verify chain with bitwise-OR accumulation
and sentinel counter. A single voltage glitch can no longer skip one
branch to fall through to SIG_OK — attacker must now corrupt all three
verify results AND the sentinel.

Add double SHA-256 computation with constant-time memcmp_s comparison
to detect transient faults during hash computation.

References: VULN-21020, fault-injection-assessment.md finding F3
…eck (F5)

Remove redundant ~1 second full ECDSA re-verification in firmware main.
The bootloader already performed authoritative signature verification
before jumping here. Replace with fast metadata presence check that
validates signature indices are present and distinct.

Eliminates the wide timing window (VULN-21020 class) where a voltage
glitch during the long-running crypto re-check could bypass protections.

References: VULN-21020, fault-injection-assessment.md finding F5
Moves which_field tag assignment after the conditional memset and only
resets when the oneof variant changes. Prevents allocated memory from
being zeroed before release when PB_ENABLE_MALLOC is active.

Ref: #361
Upstream: nanopb 4fe23595732b6f1254cfc11a9b8d6da900b55b0c
The condition on line 578 was inverted — `!enforce_wordlist` meant the
wordlist check was skipped when enforce_wordlist was true (the default).
Invalid words were silently accepted and only caught later as a generic
"Invalid mnemonic" checksum error, giving users no indication which
word was wrong.

Flip the condition so that when enforce_wordlist is set, words that
fail auto-complete against the BIP39 list are rejected immediately
with a clear error before the mnemonic is finalized.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
Previously, wordlist validation only happened at finalization after all
words were entered. Invalid words were silently accepted and the user
had to complete the entire 12/24-word entry before getting a vague
"Invalid mnemonic" error with no indication which word was wrong.

Add a BIP39 check in the space handler of recovery_character() that
rejects immediately with "Word not found in BIP39 wordlist" when the
decoded characters don't match any entry. Combined with the
enforce_wordlist condition fix, both per-word and finalization
validation now work correctly.
Add Lynx cryptocurrency support to the firmware coins table.

Parameters sourced from Lynx Core (Bitcoin v26 fork with PoS):
- SLIP-44 coin type: 191 (0x800000BF)
- Address type: 45 (pubkey hash), 22 (script hash)
- SegWit enabled with bech32 prefix "lynx"
- Taproot enabled
- Standard Bitcoin xpub/ypub/zpub magic numbers
- Signed message header: "Bitcoin Signed Message:\n" (matches upstream)
- 8 decimal places, secp256k1 curve
Add BIP-85 support for deriving child mnemonics from the device seed.
Supports 12, 18, and 24 word mnemonics via HMAC-SHA512 derivation
on path m/83696968'/39'/0'/<word_count>'/<index>'.

New files:
- bip85.h/bip85.c: Core derivation using trezor-crypto primitives
- fsm_msg_bip85.h: FSM handler for GetBip85Mnemonic message

Integration: fsm.h, fsm.c, messagemap.def, firmware CMakeLists.txt
device-protocol bumped to 17b38803e (includes BIP-85 message defs)
- Reject index >= 0x80000000 to prevent hardened-bit derivation collision
- Add has_word_count/has_index field presence checks
- Change confirmation text from "Derive" to "Export" to convey secret export
- Add second confirmation before sending child mnemonic to host
Required proto fields don't generate has_ members in nanopb.
word_count and index are guaranteed present by the proto schema.

Co-Authored-By: Claude Opus 4.6 <[email protected]>
BitHighlander and others added 7 commits March 16, 2026 21:24
Add Ed25519-based Solana chain support using existing trezor-crypto
primitives (ed25519, base58).

- SolanaGetAddress: derive Ed25519 key, Base58-encode pubkey
- SolanaSignTx: parse transaction, confirm on device, Ed25519 sign
- SolanaSignMessage: off-chain message signing with device confirm
- Transaction parser: System Transfer, SPL Token, Stake instructions
…,M6)

- Fix compact-u16 decode: use correct LE varint (bit 7 continuation), not big-endian ranges
- Remove duplicate compact-u16 in solana.c, share via read_compact_u16() in solana_tx.h
- Change solana_signTx from void to bool, propagate errors to FSM handler
- Default show_display to true when field not set (hardware wallet must confirm)
- Fix token transfer: check TransferChecked (len>=10) with instruction type, parse amount for both
- Reduce stack: account_keys 64→16, instructions 32→8 (saves ~3KB)
- Zero public_key after use in solana.c and solana_msg.c
- Remove dead read_u64_le function
- Fix formatLamports buffer guard: 20→30 bytes
- Guard MIN macro with #ifndef
- Add const to SolanaSignTx and SolanaSignMessage FSM params
- Fix NULL pointer dereference: remove fsm_getCoin("Solana") call (not in coin table),
  use bip32_path_to_string() directly for address display
- Add BIP44 path validation (m/44'/501'/...) to all 3 handlers
- Zero public_key stack variables on all exit paths
TRON: secp256k1 + Keccak256 address derivation, SHA256 tx signing
TON: Ed25519 address derivation with CRC16 + Base64url, Ed25519 tx signing

Both use existing trezor-crypto primitives only.
- Fix bounceable flag no-op: use ternary (0x11 vs 0x51) instead of OR
- Check ecdsa_uncompress_pubkey return value in tron_getAddress
- Add BIP44 path validation: m/44'/195'/... for Tron, m/44'/607'/... for TON
- Change signTx functions to return bool (was void, silent failures)
- Check signTx return in FSM handlers, send proper failure on error

Note: TON address derivation uses SHA-256(pubkey) which produces non-standard
addresses. Standard TON addresses require SHA-256(StateInit cell) which includes
wallet contract code — too large for firmware flash. This is a known limitation
documented for future resolution.
ton_get_address() was computing sha256(pubkey) directly, which does not
correspond to any deployable TON wallet contract. The correct TON v4r2
address is sha256(StateInit(code_cell, data_cell)) where the data cell
contains seqno=0, walletId=698983191, and the ed25519 public key.

The fix hardcodes the well-known v4r2 code cell hash and depth, computes
the data cell representation hash from the public key, then derives the
StateInit hash. This produces addresses compatible with the standard v4r2
wallet contract used by TonKeeper, MyTonWallet, and @ton/ton SDK.
Both standard and BTC-only firmware are now built on every PR/push,
not just on release tags.
… options

BIP-85 is chain-agnostic (BIP-32 derived mnemonics) and should be
available in BTC-only builds. Move bip85.c, fsm_msg_bip85.h, and
message map entries out of #ifndef BITCOIN_ONLY guards.

Also remove duplicate/conflicting entries in messages-tron.options
and messages-ton.options.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant